iT邦幫忙

2024 iThome 鐵人賽

DAY 21
0
Modern Web

一些讓你看來很強的 ORM - prisma系列 第 21 篇

Day21. 一些讓你看來很強的 ORM - prisma ( Seed Data)

  • 分享至 

  • xImage
  •  

今天要來介紹一個 DB 功能 seed data,有的時候當我們開發 application 時,很常會需要一些測試資料,那假設今天我們的測試資料很多,總不可能一個一個手動去新增,而且有時候也需要定期清楚過期的資料,為了簡化這些事提高開發效率,所以這時候 seed data 就派上用場了,以下我們將慢慢介紹~

介紹

seed data 大致上有以下的功能:

  • 提供 application 需要的測試資料
  • 驗證資料重複性與正確性,同時在你 migrate DB 時候 reset 你的測試資料

所以 seed data 就是讓你開發時候有資料可以測試外,同時 migrate 時候也會自動重新塞新的資料,解決 schema 不一致的問題

Demo

在 prisma 中如果要使用 seed data 功能,需要在 package.json 中加上 seed 的 key 在 prisma 這個欄位,然後當你要執行 seed data 的時候你只要run prisma db seed,如此就會知道你 seed data 的檔案在哪裡

// package.json
"prisma": {
  "seed": "ts-node prisma/seed.ts"
},

補充

如果使用 ts-node 的話要注意一件事情,就是 ts-node 他會幫你做 transpiling 跟 typechecking ,那其實 typechecking 是可以被 disabled ,只需要在你的 cli 加上 --transpile-only ,這樣 ts-node 就不會執行 typechecking ,這個參數非常好用因為他可以減少 memory (RAM) 的使用,同時加快 seed data 的執行效率

"seed": "ts-node --transpile-only prisma/seed.ts"

Integrated seeding with Prisma Migrate

Datebase seeding 在 prisma 中有兩種方式 :

  • 透過 prisma db seed 手動載入
  • prisma migrate reset 自動執行 seed data 或是 prisma migrate dev (某些情況)

使用 prisma db seed 手動方式,他的好處在於你可以測試 seed data 是否正確同時也會是你在開發前的前置作業需要確認的事情

另外當你 prisma db seed 都沒問題的時候,就可以透過 prisma migrate reset 自動執行,他會有以下的步驟:

  • 手動執行 prisma migrate reset 這個 cli
  • database 會執行 prisma migrate dev 去跟去你 migration 的記錄解決 schema 的衝突
  • 最後自動執行 prisma db seed

另外假設你不需要 seed data 在你執行,在 prisma migrate reset 或是 prisma migrate dev 只要加上 skip-seed 這個 flag 就好

Demo

以下是今天的 model

model User {
  id           String @id @default(cuid())
  name         String
  age          Int?
  profileViews Int
  country      String
  city         String
  email        String @unique
}

之後我們在 prisma 這個資料夾新增 seed.ts 的檔案,這邊簡單解釋一下 seed.ts 做了什麼事情:

  • 透過 @faker-js 產生 mock data
  • 使用 prisma.user.deleteMany 每次清空 seed data
  • 使用 prisma.user.upsert 當 email 有存在就跳過 craete ,確保 seed data 過程中 email 可能會有重複,原因是我們的 email 的 schema 是 unique 的
//prisma/seed.ts

import { faker } from "@faker-js/faker";
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient()

const main = async () => {
  await prisma.user.deleteMany({})
  for (let i = 0; i < 20; i++) {
    const userEmail = faker.internet.email()
    await prisma.user.upsert({
      where: {
        email: userEmail
      },
      update: {},
      create: {
        email: userEmail,
        name: faker.person.fullName(),
        age: faker.helpers.maybe(() => faker.number.int({ min: 0, max: 100 }), { probability: 0.8 }),
        profileViews: faker.number.int({ min: 0, max: 5000 }),
        country: faker.location.country(),
        city: faker.location.city()
      }
    })
  }
}

main()
  .then(async () => {
    await prisma.$disconnect()
  })
  .catch(async (e) => {
    console.error(e)
    await prisma.$disconnect()
    process.exit(1)
  })

那因為在 age 我們的 schema 是 optional 的,所以為了讓 seed 的更真實,@faker-js 有提供 maybe 的用法,有機率的 return data 用這個 utils 來模擬 user 沒有 age 的資料

所以以下的 code 會是有八成的機率 age 會有資料,否則會是 null

//..
age: faker.helpers.maybe(() => faker.number.int({ min: 0, max: 100 }), { probability: 0.8 })
//..

之後到 package.json 加上 prisma script

//package.json
{
  "name": "my-project",
  "version": "1.0.0",
  "prisma": {
    "seed": "ts-node prisma/seed.ts"
  },
  "devDependencies": {
    "@types/node": "^14.14.21",
    "ts-node": "^9.1.1",
    "typescript": "^4.1.3"
  }
}

執行 seed script

>npx prisma db seed

接著我們到 prisma studio 檢查一下確實 age 有些資料是 null 的

https://ithelp.ithome.com.tw/upload/images/20241006/201456776aUSHUTosg.png

另外 ts-node 有提供不同 compiler 的選項,如果你是使用 Nextjs 的話需要根據以下的寫法去轉換一下 module

"prisma": {
  "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
},

Raw SQL

其實在 prisma 中你還可以透過 Raw SQL 幫你 seed data ,使用 prisma.$executeRaw 就可以幫你完成

async function rawSql() {
  const result = await prisma.$executeRaw`INSERT INTO "User" ("id", "email", "name") VALUES (3, 'foo@example.com', 'Foo') ON CONFLICT DO NOTHING;`
  console.log({ result })
}

這邊你可以透過 Raw SQL 幫你新增額外的 seed data

main()
  .then(rawSql)
  .then(async () => {
    await prisma.$disconnect()
  })
  .catch(async (e) => {
    console.error(e)
    await prisma.$disconnect()
    process.exit(1)
  })

Seed With @snaplet/seed

另外還有一個今天的主角 @snaplet/seed 他也是另外一套 seed data 的 toolkit 他的好處是可以自動幫你提供 seed data 的 type 外,同時也幫你優化 seed data 的效率,然後 @snaplet/seed 目前支援 PostgreSQL 、SQLite and MySQL

使用前要先 init @snaplet/seed

>npx @snaplet/seed init prisma/seed 

他會幫你在 prisma 資料夾中產生以下的檔案:

  • seed.config.ts : seed 相關的 config
  • seed.ts : 執行 seed 的位置
    https://ithelp.ithome.com.tw/upload/images/20241006/20145677Z3eFAaw51Q.png

我們可以看到 seed.config.ts 主要就是透過 adapter 去 connect PrismaClient

//seed.config.ts
import { SeedPrisma } from "@snaplet/seed/adapter-prisma";
import { defineConfig } from "@snaplet/seed/config";
import { PrismaClient } from "@prisma/client";

export default defineConfig({
  adapter: () => {
    const client = new PrismaClient();
    return new SeedPrisma(client);
  },
  select: ["!*_prisma_migrations"],
});

@snaplet/seed 的寫法很單純,他會自動幫你判別你的 model 需要什麼欄位,自動幫你填上,這邊簡單解釋一下 code :

  • $resetDatabase 每次都清空 DB
  • await seed.user((createMany) => createMany(10)) 創建10筆 user 的資料
// import { createSeedClient, SeedClient } from "@snaplet/seed";
const main = async () => {

  // Truncate all tables in the database
  await seed.$resetDatabase();

  await seed.user((createMany) => createMany(10));

  // Type completion not working? You might want to reload your TypeScript Server to pick up the changes

  console.log(`Database seeded successfully!`);

  process.exit();
};

main()
  .then(async () => {
    await prisma.$disconnect()
  })
  .catch(async (e) => {
    console.error(e)
    await prisma.$disconnect()
    process.exit(1)
  })

這邊讀者打算把 ts-node 改成 tsx ,因為 tsx 他可以讓你在執行時候不用考慮 module 問題~

>npm install -D tsx

最後別忘記到 package.json 中修改一下 prisma 的 script

//package.json
{
  "name": "my-project",
  "version": "1.0.0",
  "prisma": {
    "seed": "tsx prisma/seed/seed.ts"
  },
  "devDependencies": {
    "@types/node": "^14.14.21",
    "tsx": "^4.7.2",
    "typescript": "^4.1.3"
  }
}

執行 seed data

>npx prisma db seed

但是筆者在查看 studio 後,發現資料內容,只是單純的塞假文字進去,這樣的 seed data 沒有還原真實資料的情況

https://ithelp.ithome.com.tw/upload/images/20241006/20145677pExzzHPZjr.png

目前筆者找到解決方式有兩種,一種是塞 env ,因為 @snaplet/seed 有支援 openai,透過 openai 優化你 response 部分

//.env
OPENAI_API_KEY="your_token"

另外一種就是透過 @faker-js ,這邊我們寫一個 seedData function ,然後你會發現我們是透過迴圈方式,去 createMany 一筆資料,那是因為假如是 createMany(100) ,對 @snaplet/seed 來說他實際並不是跑 100 次的 query 而是只有一次

const seedData = async ({
  seed,
  count
}: {
  seed: SeedClient,
  count: number
}) => {
  for (let i = 0; i < count; i++) {
    await seed.user((createMany) => createMany(1, {
      name: faker.person.fullName(),
      age: faker.number.int({ min: 0, max: 100 }),
      profileViews: faker.number.int({ min: 0, max: 5000 }),
      country: faker.location.country(),
      city: faker.location.city(),
      email: faker.internet.email()
    }));
  }
}

那這樣透過 faker 出來的資料因為執行次數的關係,資料都是一樣的,所以筆者才會改成遞迴方式,而且這樣的 email 也會有問題就不會是唯一性了

 await seed.user((createMany) => createMany(100, {
      name: faker.person.fullName(),
      age: faker.number.int({ min: 0, max: 100 }),
      profileViews: faker.number.int({ min: 0, max: 5000 }),
      country: faker.location.country(),
      city: faker.location.city(),
      email: faker.internet.email()
    }));

https://ithelp.ithome.com.tw/upload/images/20241006/20145677ehkDZzW5Bp.png

完整的 code 如下

const main = async () => {
    
  const seed = await createSeedClient();

  // Truncate all tables in the database
  await seed.$resetDatabase();
  await seedData({ seed, count: 20 })

  // Type completion not working? You might want to reload your TypeScript Server to pick up the changes

  console.log(`Database seeded successfully!`);

  process.exit();
};

另外如果你想自動執行 seed 的話,只需要在 package.json 的 script 加上 migrate 跟 postmigrate

//package.json
{
  "name": "my-project",
  "version": "1.0.0",
  "prisma": {
    "seed": "npx tsx prisma/seed/seed.ts"
  },
  "scripts": {
    "migrate": "prisma migrate dev",
    "postmigrate": "npx @snaplet/seed sync"
  },
  "devDependencies": {
    "@types/node": "^14.14.21",
    "tsx": "^4.7.2",
    "typescript": "^4.1.3"
  }
}

這樣當你 npm run migrate 就會幫你自動 seed data 了

>npm run migrate  
 > prisma@1.0.0 migrate
 > prisma migrate dev

Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": SQLite database "dev.db" at "file:./dev.db"

Already in sync, no schema change or pending migration was found.

✔ Generated Prisma Client (v5.19.1) to ./node_modules/@prisma/client in 54ms



> prisma@1.0.0 postmigrate
> npx @snaplet/seed sync

Dynamic Seed Data

最後補充一個有趣的小 tips 我們其實可以根據下不同的 flag 在執行 prisma db seed 的時候,有不同的 seed data 的結果,只需要用 nodejs 原生的parseArgs 去讀取 cli 的參數,以下的範例就是區分 environment 有 development 跟 test,development 有20筆資料 test 則是只有10筆,那讀者可以根據不同情況調整需求~

import { faker } from "@faker-js/faker";
import { PrismaClient } from "@prisma/client";

import { createSeedClient, SeedClient } from "@snaplet/seed";
import { parseArgs, ParseArgsConfig } from "util";

const main = async () => {
  const {
    values: { environment },
  } = parseArgs({ options })
  const seed = await createSeedClient();

  // Truncate all tables in the database
  await seed.$resetDatabase();

  switch (environment) {
    case 'development':
      seedData({ seed, count: 20 })
      break
    case 'test':
      seedData({ seed, count: 10 })
      break
    default:
      break
  }

  // Type completion not working? You might want to reload your TypeScript Server to pick up the changes

  console.log(`environment(${environment}): Database seeded successfully!`);

  process.exit();
};

最後只要下以下的 cli 就成功了~

>npx prisma db seed -- --environment development

大家如果有問題可以來小弟的群組討論~

✅ 前端社群 :
https://lihi3.cc/kBe0Y


上一篇
Day20. 一些讓你看來很強的 ORM - prisma (Omit Fields )
下一篇
Day22. 一些讓你看來很強的 ORM - prisma ( Logging & Debug)
系列文
一些讓你看來很強的 ORM - prisma 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言